package cn.wenhaha.app.job.manager;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.db.Entity;
import cn.wenhaha.app.data.service.DataPluginService;
import cn.wenhaha.app.data.service.SyncPluginService;
import cn.wenhaha.app.job.core.DataSource;
import cn.wenhaha.app.job.core.DataSyncCore;
import cn.wenhaha.app.job.kit.ConfigKit;
import cn.wenhaha.app.job.kit.MappingKit;
import cn.wenhaha.app.job.script.JsScript;
import cn.wenhaha.app.log.bean.LogMapping;
import cn.wenhaha.app.log.bean.ReportErrorType;
import cn.wenhaha.app.log.service.SyncLogService;
import cn.wenhaha.app.mapping.bean.MappingType;
import cn.wenhaha.app.mapping.bean.dto.*;
import cn.wenhaha.common.db.bean.SysMapping;
import cn.wenhaha.common.utils.AddressUtil;
import cn.wenhaha.job.WynJobBean;
import cn.wenhaha.sync.core.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import static cn.wenhaha.sync.core.QueryType.*;

/**
 * 同步任务
 * --------
 *
 * @author ：wyndem
 * @Date ：Created in 2022-11-01 22:49
 */
@DisallowConcurrentExecution
public class SyncMappingJob extends WynJobBean {

    private final Logger logger = LoggerFactory.getLogger(SyncMappingJob.class);

    /**
     * 数据源插件
     **/
    @Resource
    private DataPluginService dataPluginService;

    /**
     * 同步核心插件
     **/
    @Resource
    private SyncPluginService syncPluginService;

    @Resource
    private ObjectMapper objectMapper;

    @Resource
    private SyncLogService logService;

    private SysMapping sysMapping;

    public SyncMappingJob() {
    }

    public SyncMappingJob(SysMapping sysMapping) {
        this.sysMapping = sysMapping;
    }

    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
        sysMapping = (SysMapping) jobDataMap.get(SysMapping.class.getName());
        run();
    }


    public void run() {


        // 获取同步支持库
        DataSyncCoreContext syncCoreContext = null;
        DataSyncCoreContext syncCoreContext2 = null;
        try {
            syncCoreContext = syncPluginService.getPluginId(sysMapping.getSyncCore1());
            syncCoreContext2 = syncPluginService.getPluginId(sysMapping.getSyncCore2());
        } catch (Exception e) {
            transferException(e, sysMapping, null, null);
            throw e;
        }

        try {
            MappingContext mappingContext = objectMapper.readValue(sysMapping.getMapping(), MappingContext.class);
            ConfigContext configContext = objectMapper.readValue(sysMapping.getConfig(), ConfigContext.class);
            Condition condition = configContext.getCondition();
            WriteBack writeBack = configContext.getWriteBack();
            WriteBackItem success = writeBack.getSuccess();
            WriteBackItem fail = writeBack.getFail();

            DataSource dx = new DataSource();
            dx.setSyncCoreContext(syncCoreContext);
            dx.setSource(mappingContext.getSource1());
            dx.setCondition(condition.getSource1());
            dx.setWritesSuccess(success.getSource1());
            dx.setWritesFail(fail.getSource1());
            dx.setCode(sysMapping.getSource1Code());
            dx.setSourceId(Integer.parseInt(sysMapping.getSource1Id()));
            dx.setSourceObj(sysMapping.getSource1Obj());

            DataSource dy = new DataSource();
            dy.setSyncCoreContext(syncCoreContext2);
            dy.setSource(mappingContext.getSource2());
            dy.setCondition(condition.getSource2());
            dy.setWritesSuccess(success.getSource2());
            dy.setWritesFail(fail.getSource2());
            dy.setCode(sysMapping.getSource2Code());
            dy.setSourceId(Integer.parseInt(sysMapping.getSource2Id()));
            dy.setSourceObj(sysMapping.getSource2Obj());

            logger.info("映射id为{}，开始同步", sysMapping.getId());

            // y 到 x
            try {
                dataTransfer(dy, dx);
            } catch (Exception e) {
                transferException(e, sysMapping, dy.getSourceObj(), dx.getSourceObj());
            }
            // x 到 y
            try {
                dataTransfer(dx, dy);
            } catch (Exception e) {
                transferException(e, sysMapping, dx.getSourceObj(), dy.getSourceObj());
            }

        } catch (Exception e) {
            e.printStackTrace();
            logger.error("同步出错", e);
        }


    }


    private void transferException(Exception e, SysMapping sysMapping, String dataName, String uploadName) {
        logger.error("当同步发生异常:", e);
        e.printStackTrace();
        LogMapping logMapping = LogMapping.builder()
                .mappingName(sysMapping.getName())
                .mappingId(Integer.parseInt(sysMapping.getId()))
                .nameData(dataName)
                .nameUpload(uploadName)
                .build();

        SyncErrorReport syncErrorReport = new SyncErrorReport(null, null, null,
                null, e.getMessage(), e, null);
        logService.add(logMapping, CollUtil.toList(syncErrorReport), ReportErrorType.SYSTEM);
    }


    /**
     * <p>
     * 转换符合上传方的数据
     * 1  查询的数据
     * *     2 上传的结构体
     * *     3 上传的对象名
     * *     4 当前的主键名
     * *     5 是否删除主键
     * </p>
     *
     * @param data      已查询到的数据
     * @param source    与上传方映射的字段关系
     * @param objName   上传方的对象名称
     * @param keyField  查询方主键字段
     * @param removeKey 是否要删除主键字段
     * @Author: Wyndem
     * @DateTime: 2022-12-01 22:21
     */
    private void convUpload(List<Entity> data, List<MappingObj> source, String objName, String keyField, boolean removeKey) {

        // 额外需要脚本的字段映射
        Map<String, List<MappingObj>> jsMap = source.stream().filter(field -> {
            String script = StrUtil.trim(field.getScript());
            return StrUtil.isNotEmpty(script);
        }).collect(Collectors.groupingBy(MappingObj::getNameApi));

        // 普通的字段映射
        List<MappingObj> dbSource = source.stream().filter(field -> {
            MappingType mappingType = MappingType.findByQueryType(field.getType());
            if (mappingType == null) {
                return false;
            }
            return mappingType == MappingType.column && StrUtil.isNotEmpty(field.getColumn()) && !jsMap.containsKey(field.getNameApi());
        }).collect(Collectors.toList());
        // 特殊的字段映射
        List<Column> mapping = MappingKit.querySyncFields(source).stream()
                .filter(field -> field.getType() != ColumnType.column)
                .collect(Collectors.toList());

        JsScript jsScript = new JsScript();


        for (Entity e : data) {
            e.setTableName(objName);
            Entity entity = new Entity();
            List<String> removeList = new ArrayList<>();


            jsMap.forEach((k, v) -> {
                MappingObj mp = v.get(0);
                try {
                    Object execute = jsScript.execute(mp.getScript(), e);
                    removeList.add(mp.getColumn());
                    entity.put(mp.getNameApi(), execute);
                } catch (Exception ex) {
                    throw new RuntimeException(ex);
                }
            });

            if (removeKey) {
                e.remove(keyField);
            }

            // 与普通的字段进行映射关联
            for (MappingObj mp : dbSource) {
                if (e.containsKey(mp.getColumn())) {
                    Object o = e.get(mp.getColumn());
                    removeList.add(mp.getColumn());
                    entity.put(mp.getNameApi(), o);
                }
            }
            //删除已关联的字段
            for (String string : removeList) {
                e.remove(string);
            }
            //与特殊字段进行关联
            for (Column c : mapping) {
                String column = c.getColumn();
                int i = column.lastIndexOf("_");
                Object remove = e.remove(column);
                String nameApi = column.substring(0, i);
                if (!entity.containsKey(nameApi)) {
                    e.put(nameApi, remove);
                }
            }

            e.putAll(entity);
        }

    }


    /**
     * <p>
     * 回写
     * </p>
     *
     * @param writeObjs 回写策略
     * @param data      数据源
     * @param x         提供方
     * @param y         上传方
     * @Author: Wyndem
     * @DateTime: 2022-12-04 15:24
     */
    private Entity writes(List<WriteObj> writeObjs, Entity data, DataSyncCore x, DataSyncCore y) {

        Entity entity = new Entity();
        entity.setTableName(x.getSourceObj());

        Map<String, List<Column>> columnMap = new HashMap<>();
        for (WriteObj writeObj : writeObjs) {
            if (!ConfigKit.isWriteValue(writeObj)) {
                continue;
            }
            QueryType queryType = QueryType.findByQueryType(Integer.valueOf(writeObj.getType()));

            if (queryType == NUILL) {
                entity.put(writeObj.getField(), null);
            }

            if (queryType == STRING || queryType == NUMBER) {
                entity.put(writeObj.getField(), writeObj.getValue());
            }

            if (queryType == Custom) {
                String customSql = StrUtil.format(writeObj.getValue().toString(), data);
                if (!customSql.contains(":")) {
                    entity.put(writeObj.getField(), customSql);
                } else {
                    String type = customSql.substring(0, customSql.indexOf(":"));
                    Column build = Column.builder()
                            .column(writeObj.getField())
                            .type(ColumnType.custom)
                            .value(customSql)
                            .build();
                    if (columnMap.containsKey(type)) {
                        columnMap.get(type).add(build);
                    } else {
                        columnMap.put(type, CollUtil.toList(build));
                    }
                }
            }
        }
        if (entity.size() == 0) {
            return null;
        }

        columnMap.forEach((k, v) -> {
            Entity dataEntity = null;
            if (x.isEchoQuery(k)) {
                dataEntity = x.echoQuery(v);
            } else if (y.isEchoQuery(k)) {
                dataEntity = y.echoQuery(v);
            }
            if (dataEntity != null) {
                entity.putAll(dataEntity);
            }
        });

        return entity;
    }


    /**
     * <p>
     * 数据传输
     * </p>
     *
     * @param dataSource 数据提供方
     * @param upload     数据上传
     * @Author: Wyndem
     * @DateTime: 2022-12-01 21:42
     */
    private void dataTransfer(DataSource dataSource, DataSource upload) {
        List<Column> columns = MappingKit.querySyncFields(upload.getSource());
        if (CollUtil.isEmpty(columns)) {
            logger.error("{}-->对象暂时没有设置同步字段，不去同步", dataSource.getSourceObj());
            return;
        }
        logger.info("正在同步{}到{}", dataSource.getSourceObj(), upload.getSourceObj());

        MappingObj dataSourceKey = MappingKit.getKeyFileName(dataSource.getSource());
        MappingObj uploadKey = MappingKit.getKeyFileName(upload.getSource());

        LogMapping logMapping = LogMapping.builder()
                .mappingName(sysMapping.getName())
                .mappingId(Integer.parseInt(sysMapping.getId()))
                .nameData(dataSource.getSourceObj())
                .nameUpload(upload.getSourceObj())
                .build();


        DataSyncCore syncCoreContext = new DataSyncCore(dataSource, logMapping);
        DataSyncCore uploadContext = new DataSyncCore(upload, logMapping);
//        获取查询条件
        List<Query> collect = ConfigKit.getCondition(dataSource.getCondition());


        boolean addKey = addKeyField(dataSourceKey, columns);
        // 这里查询的数据 如果是字段之间的映射还没有变成上传方的符合要求的字段
        logger.info("{}正在查询待同步对象", dataSource.getSourceObj());
        List<Entity> query = syncCoreContext.query(columns, collect);
        if (CollUtil.isEmpty(query)) {
            logger.info("{}-->对象暂时没有可同步的数据", dataSource.getSourceObj());
            return;
        }

        Map<Long, Object> keyFieldValue = getKeyFieldValue(query, dataSourceKey);
        convUpload(query, upload.getSource(), upload.getSourceObj(), dataSourceKey.getNameApi(), addKey);

        logger.info("{}正在上传至{}", dataSource.getSourceObj(), upload.getSourceObj());
        logger.info("{}待同步数为: {}", dataSource.getSourceObj(), query.size());
        List<SyncReport> uploadData = uploadContext.upload(uploadKey.getNameApi(), query);
        if (CollUtil.isEmpty(uploadData)) {
            logger.info("反馈消息为空，后续不做回写等处理..");
            return;
        }

        //处理回写， 顺序问题，使用id来解决
        List<SyncSuccessReport> success = uploadData.stream()
                .filter(r -> r instanceof SyncSuccessReport)
                .map(r -> (SyncSuccessReport) r)
                .collect(Collectors.toList());

        List<Entity> writeData = new ArrayList<>();
        List<WriteObj> writesSuccess = dataSource.getWritesSuccess();
        for (SyncSuccessReport report : success) {
            Entity data = report.getData();
            Object o = keyFieldValue.get(AddressUtil.addressOf(data));
            data.put("x.id", o);
            data.put("y.id", report.getDataId());
            Entity writes = writes(writesSuccess, data, syncCoreContext, uploadContext);
            if (writes != null) {
                writes.put(dataSourceKey.getNameApi(), o);
                writeData.add(writes);
            }

        }
        if (CollUtil.isNotEmpty(writeData)) {
            logger.info("{}正在回写成功字段", dataSource.getSourceObj());
            // 回写
            syncCoreContext.uploadWrite(dataSourceKey.getNameApi(), writeData);
        }


        writeData.clear();
        List<SyncErrorReport> errorReport = uploadData.stream()
                .filter(r -> r instanceof SyncErrorReport)
                .map(r -> (SyncErrorReport) r)
                .collect(Collectors.toList());
        List<WriteObj> writesFail = dataSource.getWritesFail();
        logger.info("{}同步失败数为: {}", dataSource.getSourceObj(), errorReport.size());
        logger.info("{}同步成功数为: {}", dataSource.getSourceObj(), success.size());

        for (SyncErrorReport report : errorReport) {
            Entity data = report.getData();
            Object o = keyFieldValue.get(AddressUtil.addressOf(data));
            data.put("x.id", o);
            Entity writes = writes(writesFail, data, syncCoreContext, uploadContext);
            if (writes != null) {
                writes.put(dataSourceKey.getNameApi(), o);
                writeData.add(writes);
            }

        }
        if (CollUtil.isNotEmpty(writeData)) {
            logger.info("{}正在回写失败字段", dataSource.getSourceObj());
            // 回写
            syncCoreContext.uploadWrite(dataSourceKey.getNameApi(), writeData);
        }
        logger.info("{}同步完成!!", dataSource.getSourceObj());

    }


    /**
     * <p>
     * 提取Entity中的Id值
     * </p>
     *
     * @Author: Wyndem
     * @DateTime: 2022-12-07 22:31
     */
    private Map<Long, Object> getKeyFieldValue(List<Entity> data, MappingObj key) {
        Map<Long, Object> idMap = new HashMap<>(data.size());
        data.forEach(d -> {
            Object o = d.get(key.getNameApi());
            idMap.put(AddressUtil.addressOf(d), o);
        });
        return idMap;
    }


    /**
     * <p>
     * 如果不存在 则添加主键
     * </p>
     *
     * @Author: Wyndem
     * @DateTime: 2022-12-01 22:10
     */
    private boolean addKeyField(MappingObj keyFileName, List<Column> columns) {
        long count = columns.stream()
                .filter(c -> StrUtil.equals(keyFileName.getNameApi(), c.getColumn()))
                .count();
        if (count == 0) {
            columns.add(Column.builder()
                    .column(keyFileName.getNameApi())
                    .type(ColumnType.column)
                    .build());
        }
        return count == 0;
    }


}
